#include "stdafx.h"
#include "cmdlineoptions.h"
#include "utils.h"
#include "getopt.h"
#include <codecvt>

using namespace std;

bool CCmdLineOptions::Parse(int argc, wchar_t** argv, ILog* log)
{
	static const struct option long_options[] =
	{
		{ _T("source"), required_argument, 0, _T('s') },
		{ _T("randomsource"), required_argument, 0, _T('r') },
		{ _T("command"), required_argument, 0, _T('c') },
		{ _T("engine"), required_argument, 0, _T('e') },
		{ _T("layout"), required_argument, 0, _T('l') },
		{ _T("transformation_type"), required_argument, 0, _T('t') },
		{ _T("outputfmt"), required_argument, 0, _T('o') },
		{ _T("outputpath"), required_argument, 0, _T('p') },
		{ _T("outputfilename"), required_argument, 0, _T('f') },
		{ _T("normalization"), required_argument, 0, _T('n') },
		{ _T("benchmark"), required_argument, 0, _T('b') },
		{ _T("adddllpath"), required_argument, 0, _T('a') },
		{ _T("help"), no_argument, 0, _T('?') },
		{ 0, 0, 0, 0 }
	};

	// set defaults
	this->command = Command::Transform;
	this->fftEngine = FFTEngine::IppFFT;
	this->ftDataLayout = FourierTransformedDataLayout::Complex;
	this->transformationType = TransformationType::R2C;

	for (;;)
	{
		int option_index;
		int c = getopt_long(argc, argv, L"?s:r:c:e:l:t:o:p:f:n:b:a:", long_options, &option_index);
		if (c == -1)
		{
			break;
		}

		switch (c)
		{
		case _T('?'):
			this->PrintUsage(log);
			return false;
		case _T('s'):
			this->sourceFilename = optarg;

			// the latest argument (among 's' or 'r') wins
			this->randSrcWidth = this->randSrcHeight = 0;
			break;
		case _T('r'):
		{
			auto randData = ParseRandomSource(optarg);
			this->randSrcWidth = get<0>(randData);
			this->randSrcHeight = get<1>(randData);
			this->randomGenMin = get<2>(randData);
			this->randomGenMax = get<3>(randData);
			this->randomSeed = get<4>(randData);
			this->sourceFilename.clear();
		}

		break;
		case _T('c'):
			this->command = ParseCommand(optarg);
			break;
		case _T('e'):
			this->fftEngine = FFTEngineStringToEnum(optarg);
			break;
		case _T('l'):
			this->ftDataLayout = DataLayoutStringToEnum(optarg);
			break;
		case _T('t'):
			this->transformationType = TransformationTypeStringToEnum(optarg);
			break;
		case _T('o'):
			this->outputFileFmt = OutputFileFmtStringToEnum(optarg);
			break;
		case _T('p'):
			this->outputPath = optarg;
			break;
		case _T('f'):
			this->outputFileName = optarg;
			break;
		case _T('n'):
			this->normalizationFactor = ParseNormalizationFactor(optarg);
			break;
		case _T('b'):
			this->benchmarkRepeatCount = ParseBenchmarkRepeatCount(optarg);
			break;
		case _T('a'):
			this->AddDllSearchPath(optarg);
			break;
		default:
			break;
		}
	}

	return true;
}

void CCmdLineOptions::PrintUsage(ILog* log)
{
	const wchar_t* synopsis =
		L"Usage: %s [options]\n"
		L"\n"
		L"Options:\n"
		L"  --source <file>, -s           Read the source data from the specified file.\n"
		L"  --randomsource <args>, -r     Use random numbers as source, the format of\n"
		L"                                the argument <args> is \n"
		L"                                \"widthxheight min=.. max=.. seed=..\".\n"
		L"                                width and height give the size of the matrix;\n"
		L"                                min and max are the lower and upper limit for\n"
		L"                                random numbers; and seed gives the seed for\n"
		L"                                the random generator (integer). The following\n"
		L"                                defaults are used: min=0.0 max=1.0 seed=-1.\n"
		L"  --command <transform|listversions>, -c\n"
		L"                                Specifies the operation to perform. Possible\n"
		L"                                values are\n"
		L"                                 \"transform\" -> perform a Fourier Transform\n"
		L"                                 \"listversions\" -> give information about\n"
		L"                                                   the FT-engines available\n"
		L"                                 \"passthroughsource\" -> pass the source\n"
		L"                                                        through unmodified\n"
		L"  --engine <enginename>, -e     Specifies the engine to use for the Fourier\n"
		L"                                Transformation.\n"
		L"  --layout <name>, -l           Specifies the layout of the output data,\n"
		L"                                possible values are \"PackedFFTW\", \"PackedIPP\",\n"
		L"                                \"Complex\", \"ComplexZeroFreqAtCenter\".\n"
		L"  --transformation_type <type>, -t\n"
		L"                                Specifies the type of transformation to be\n"
		L"                                executed, possible values are\n"
		L"                                 \"R2C\" -> real-to-complex Fourier Transform\n"
		L"                                           (implicit forward)\n"
		L"                                 \"C2C_FWD\" -> complex-to-complex forward\n"
		L"                                                Fourier Transform\n"
		L"                                 \"C2C_INV\" -> complex-to-complex inverse\n"
		L"                                                Fourier Transform\n"
		L"                                 \"C2R\" -> complex-to-real Fourier Transform\n"
		L"                                          (implicit inverse)\n"
		L"  --outputfmt <fmt>, -o         Specifies the output format, possible values\n"
		L"                                are\n"
		L"                                 \"CSV\" -> a CSV text file\n"
		L"                                 \"BINARY\" -> a binary file\n"
		L"  --outputpath <path>, -p       Gives the path where to write the output files.\n"
		L"  --outputfilename <name>, -f   Gives the name of the output file(s).\n"
		L"  --normalization <value>, -n   Specifies the normalisation factor, the\n"
		L"                                argument may either be a number or \"size\" or\n"
		L"                                \"sqrt_size\".\n"
		L"  --benchmark <repeats>, -b     Benchmark the operation and repeat it as many\n"
		L"                                times as specified here.\n"
		L"  --adddllpath <path>, -a       Use the specified path when trying to load\n"
		L"                                additional DLLs\n"
		L"  --help, -?                    Print this help text.\n";

	log->WriteStdOut(synopsis, CCmdLineOptions::GetExeName().c_str());
}

/*static*/std::wstring CCmdLineOptions::GetExeName()
{
	wchar_t* programName;
	_get_wpgmptr(&programName);
	const wchar_t* exeName = _tcsrchr(programName, L'/');
	if (exeName == nullptr)
	{
		exeName = _tcsrchr(programName, L'\\');
	}

	if (exeName == nullptr)
	{
		return wstring(L"unknown");
	}

	return wstring(1 + exeName);
}

uint32_t CCmdLineOptions::GetBenchmarkRepeatCount() const
{
	return this->benchmarkRepeatCount;
}

bool CCmdLineOptions::DoBenchmark() const
{
	return this->benchmarkRepeatCount > 0;
}

Command CCmdLineOptions::GetCommand() const
{
	return this->command;
}

CCmdLineOptions::CCmdLineOptions() 
{
	this->SetDefaults();
}

void CCmdLineOptions::SetDefaults()
{
	this->benchmarkRepeatCount = 0;
	this->normalizationFactor = 1.0;
	this->randSrcWidth = 0;
	this->randSrcHeight = 0;
}

const std::wstring& CCmdLineOptions::GetSourceFilename() const
{
	return this->sourceFilename;
}

const std::vector<std::wstring>& CCmdLineOptions::GetAdditionalDllSearchPath() const
{
	return this->additionalDllSearchPath;
}

double CCmdLineOptions::GetNormalizationFactor() const
{
	return this->normalizationFactor;
}

bool CCmdLineOptions::GetRandomSrcParameters(uint32_t* pWidth, uint32_t* pHeight, float* pRandMin, float* pRandMax, uint32_t* pRandSeed) const
{
	if (this->randSrcWidth <= 0 || this->randSrcHeight <= 0)
	{
		return false;
	}

	if (pWidth != nullptr)
	{
		*pWidth = this->randSrcWidth;
	}

	if (pHeight != nullptr)
	{
		*pHeight = this->randSrcHeight;
	}

	if (pRandMin != nullptr)
	{
		*pRandMin = this->randomGenMin;
	}

	if (pRandMax != nullptr)
	{
		*pRandMax = this->randomGenMax;
	}

	if (pRandSeed != nullptr)
	{
		*pRandSeed = this->randomSeed;
	}

	return true;
}

std::wstring CCmdLineOptions::GenerateOutputFilename(OutputFileNameType type, const wchar_t* pszExtension) const
{
	std::wstring path;
	if (this->outputPath.empty())
	{
		path = CCmdLineOptions::PathFromFullyQualifiedFilename(this->GetSourceFilename());
	}
	else
	{
		path = this->outputPath;
	}

	std::wstring filenameWOExt;
	if (this->outputFileName.empty())
	{
		std::wstring filename = CCmdLineOptions::FilenameFromFullyQualifiedFilename(this->GetSourceFilename());
		CCmdLineOptions::SplitFilename(filename, &filenameWOExt, nullptr);
	}
	else
	{
		filenameWOExt = this->outputFileName;
	}

	switch (type)
	{
	case OutputFileNameType::Real_FFT:
		return ConcatPath(path, filenameWOExt) + L"_RealFFT." + pszExtension;
	case OutputFileNameType::Imag_FFT:
		return ConcatPath(path, filenameWOExt) + L"_ImagFFT." + pszExtension;
	case OutputFileNameType::CSV_Complex:
		return ConcatPath(path, filenameWOExt) + L"_complex." + pszExtension;
	case OutputFileNameType::BINARY_Complex:
		return ConcatPath(path, filenameWOExt) + L"_complex." + pszExtension;
	case OutputFileNameType::CSV_Real:
		return ConcatPath(path, filenameWOExt) + L"_real." + pszExtension;
	case OutputFileNameType::BINARY_Real:
		return ConcatPath(path, filenameWOExt) + L"_real." + pszExtension;
	default:
		break;
	}

	throw std::invalid_argument("Unknown outputfile-type.");
}

void CCmdLineOptions::GetDefaultFileExtension(OutputFormat outputFmt, std::wstring& str) const
{
	switch (outputFmt)
	{
	case OutputFormat::CSV:
		str = L"csv";
		break;
	case OutputFormat::BINARY:
		str = L"bin";
		break;
	case OutputFormat::REALCOMPLEX_WDP_FLOAT:
		str = L"wdp";
		break;
	case OutputFormat::REALCOMPLEX_PNG_GRAY16NORMALIZED:
	case OutputFormat::REALCOMPLEX_PNG_GRAY8NORMALIZED:
		str = L"png";
		break;
	default:
		throw std::invalid_argument("Unknown outputfile-type.");
		break;
	}
}

/*static*/std::wstring CCmdLineOptions::PathFromFullyQualifiedFilename(const std::wstring& filename)
{
	std::wstring::size_type s = filename.find_last_of(L"/\\:");
	if (s != std::wstring::npos)
	{
		return filename.substr(0, s);
	}

	return std::wstring();
}

/*static*/std::wstring CCmdLineOptions::FilenameFromFullyQualifiedFilename(const std::wstring& filename)
{
	std::wstring::size_type s = filename.find_last_of(L"/\\:");
	if (s != std::wstring::npos)
	{
		return filename.substr(1 + s);
	}

	return std::wstring(filename);
}

/*static*/void CCmdLineOptions::SplitFilename(const std::wstring& filename, std::wstring* fileNameWoExtension, std::wstring* extension)
{
	std::wstring::size_type s = filename.find_last_of(L".");
	if (s != std::wstring::npos)
	{
		if (fileNameWoExtension != nullptr)
		{
			*fileNameWoExtension = filename.substr(0, s);
		}

		if (extension != nullptr)
		{
			*extension = filename.substr(s + 1);
		}
	}
	else
	{
		if (fileNameWoExtension != nullptr)
		{
			*fileNameWoExtension = filename;
		}

		if (extension != nullptr)
		{
			extension->clear();
		}
	}
}

/*static*/std::wstring CCmdLineOptions::ConcatPath(const std::wstring& a, const std::wstring& b)
{
	return a + L'\\' + b;
}

/*static*/FFTEngine CCmdLineOptions::FFTEngineStringToEnum(const wchar_t* s)
{
	if (_wcsicmp(s, L"CUFFT") == 0)
	{
		return FFTEngine::CuFFT;
	}
	else if (_wcsicmp(s, L"IPPFFT") == 0)
	{
		return FFTEngine::IppFFT;
	}
	else if (_wcsicmp(s, L"FFTWFFT") == 0)
	{
		return FFTEngine::FFTWFFT;
	}
	else if (_wcsicmp(s, L"D3DCSX") == 0)
	{
		return FFTEngine::D3DCSX;
	}
	else if (_wcsicmp(s, L"CLFFT") == 0)
	{
		return FFTEngine::CLFFT;
	}

	throw std::invalid_argument("Invalid engine-type.");
}

/*static*/FourierTransformedDataLayout CCmdLineOptions::DataLayoutStringToEnum(const wchar_t* s)
{
	if (_wcsicmp(s, L"PackedFFTW") == 0)
	{
		return FourierTransformedDataLayout::PackedComplex_FftwFormat;
	}
	else if (_wcsicmp(s, L"PackedIPP") == 0)
	{
		return FourierTransformedDataLayout::PackedComplex_IppFormat;
	}
	else if (_wcsicmp(s, L"Complex") == 0)
	{
		return FourierTransformedDataLayout::Complex;
	}
	else if (_wcsicmp(s, L"ComplexZeroFreqAtCenter") == 0)
	{
		return FourierTransformedDataLayout::ComplexZeroFrequencyCentered;
	}

	throw std::invalid_argument("Invalid data layout.");
}

/*static*/OutputFormat CCmdLineOptions::OutputFileFmtStringToEnum(const wchar_t* s)
{
	if (_wcsicmp(s, L"CSV") == 0)
	{
		return OutputFormat::CSV;
	}
	else if (_wcsicmp(s, L"BINARY") == 0)
	{
		return OutputFormat::BINARY;
	}
	else if (_wcsicmp(s, L"REALCOMPLEX_WDP_FLOAT") == 0)
	{
		return OutputFormat::REALCOMPLEX_WDP_FLOAT;
	}
	else if (_wcsicmp(s, L"REALCOMPLEX_PNG_GRAY16NORMALIZED") == 0)
	{
		return OutputFormat::REALCOMPLEX_PNG_GRAY16NORMALIZED;
	}
	else if (_wcsicmp(s, L"REALCOMPLEX_PNG_GRAY8NORMALIZED") == 0)
	{
		return OutputFormat::REALCOMPLEX_PNG_GRAY8NORMALIZED;
	}

	throw std::invalid_argument("Invalid output format.");
}

/*static*/TransformationType CCmdLineOptions::TransformationTypeStringToEnum(const wchar_t* s)
{
	if (_wcsicmp(s, L"R2C") == 0)
	{
		return TransformationType::R2C;
	}
	else if (_wcsicmp(s, L"C2C_FWD") == 0)
	{
		return TransformationType::C2C_FWD;
	}
	else if (_wcsicmp(s, L"C2C_INV") == 0)
	{
		return TransformationType::C2C_INV;
	}
	else if (_wcsicmp(s, L"C2R") == 0)
	{
		return TransformationType::C2R;
	}

	throw std::invalid_argument("Invalid transformation type.");
}

FFTEngine CCmdLineOptions::GetFFTEngine() const
{
	return this->fftEngine;
}

FourierTransformedDataLayout CCmdLineOptions::GetFourierTransformDataLayout() const
{
	return this->ftDataLayout;
}

OutputFormat CCmdLineOptions::GetOutputFileFormat() const
{
	return this->outputFileFmt;
}

TransformationType CCmdLineOptions::GetTransformationType() const
{
	return this->transformationType;
}

/*static*/double CCmdLineOptions::ParseNormalizationFactor(const wchar_t* s)
{
	return ParseNormalizationFactor(std::wstring(s));
}

/*static*/double CCmdLineOptions::ParseNormalizationFactor(const std::wstring& s)
{
	// remove trailing and leading spaces
	auto str = trim_copy(s);

	if (_wcsicmp(str.c_str(), L"size") == 0)
	{
		return -std::numeric_limits<double>::infinity();
	}

	if (_wcsicmp(str.c_str(), L"sqrt_size") == 0)
	{
		return std::numeric_limits<double>::infinity();
	}

	wchar_t* endptr;
	double value = wcstod(str.c_str(), &endptr);
	if (*endptr)
	{
		// could not parse the string or some characters were left over
		return std::numeric_limits<double>::quiet_NaN();
	}

	return value;
}

/*static*/std::tuple<int, int, float, float, uint32_t> CCmdLineOptions::ParseRandomSource(const wchar_t* s)
{
	std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
	std::string converted_str = converter.to_bytes(s);
	return ParseRandomSource(converted_str);
}

/*static*/std::tuple<int, int, float, float, uint32_t> CCmdLineOptions::ParseRandomSource(const std::string& s)
{
	// format is: 720x440 min=0.3 max=1.9 seed=43433
	// 
	//			everything behind the size if optional
	std::vector<std::string> vecParts;
	split(s, " ", vecParts);

	// the first part (width x height) is mandatory
	if (vecParts.size() < 1 || vecParts[0].size() == 0)
	{
		throw std::invalid_argument("Invalid string");
	}

	std::vector<std::string> widthxheight;
	split(vecParts[0], "x", widthxheight);
	if (widthxheight.size() != 2 || widthxheight[0].size() == 0 || widthxheight[1].size() == 0)
	{
		throw std::invalid_argument("Invalid string");
	}

	int width = std::stoi(widthxheight[0]);
	int height = std::stoi(widthxheight[1]);
	if (width <= 0 || height <= 0)
	{
		throw std::invalid_argument("Invalid string");
	}

	float minValue = 0;
	float maxValue = 1;
	uint32_t seed = -1;
	for (int i = 1; i < vecParts.size(); ++i)
	{
		std::vector<std::string> v;
		split(vecParts[i], "=", v);
		if (v.size() != 2 || v[0].size() == 0)
		{
			throw std::invalid_argument("Invalid string");
		}

		if (strcmp(v[0].c_str(), "min") == 0)
		{
			minValue = std::stof(v[1]);
		}
		else if (strcmp(v[0].c_str(), "max") == 0)
		{
			maxValue = std::stof(v[1]);
		}
		else if (strcmp(v[0].c_str(), "seed") == 0)
		{
			seed = std::stoul(v[1]);
		}
		else
		{
			throw std::invalid_argument("Invalid string");
		}
	}

	return std::tuple<int, int, float, float, int>(width, height, minValue, maxValue, seed);
}

/*static*/Command CCmdLineOptions::ParseCommand(const wchar_t* s)
{
	if (_wcsicmp(s, L"transform") == 0)
	{
		return Command::Transform;
	}
	else if (_wcsicmp(s, L"listversions") == 0)
	{
		return Command::ListVersions;
	}
	else if (_wcsicmp(s, L"passthroughsource") == 0)
	{
		return Command::PassThroughSource;
	}

	throw std::invalid_argument("Invalid command.");
}

/*static*/std::uint32_t CCmdLineOptions::ParseBenchmarkRepeatCount(const wchar_t* s)
{
	wchar_t* endPtr;
	uint32_t v = wcstoul(s, &endPtr, 10);
	if (endPtr == s || !isEmpty(endPtr))
	{
		throw std::invalid_argument("Invalid number.");
	}

	return v;
}

void CCmdLineOptions::AddDllSearchPath(const wchar_t* path)
{
	auto absPath = realPath(path);
	////printf("Path=%ls  AbsPath=%ls\n", path, absPath.c_str());
	if (!absPath.empty())
	{
		this->additionalDllSearchPath.push_back(absPath);
	}
}
